package cn.org.rapid_framework.generator;

import static cn.org.rapid_framework.generator.GeneratorConstants.*;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import cn.org.rapid_framework.generator.util.AntPathMatcher;
import cn.org.rapid_framework.generator.util.BeanHelper;
import cn.org.rapid_framework.generator.util.FileHelper;
import cn.org.rapid_framework.generator.util.FreemarkerHelper;
import cn.org.rapid_framework.generator.util.GLogger;
import cn.org.rapid_framework.generator.util.GeneratorException;
import cn.org.rapid_framework.generator.util.IOHelper;
import cn.org.rapid_framework.generator.util.StringHelper;
import cn.org.rapid_framework.generator.util.ZipUtils;
import freemarker.cache.FileTemplateLoader;
import freemarker.cache.MultiTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;

/**
 * 代码生成器核心引擎 主要提供以下两个方法供外部使用
 * 
 * <pre>
 * generateBy() 用于生成文件
 * deleteBy() 用于删除生成的文件
 * </pre>
 * 
 * @author badqiu
 * @email badqiu(a)gmail.com
 */

@SuppressWarnings("all")
public class Generator {
    private static final String GENERATOR_INSERT_LOCATION       = "generator-insert-location";
    private ArrayList<File>     templateRootDirs                = new ArrayList<File>();
    private String              outRootDir;
    private boolean             ignoreTemplateGenerateException = true;
    private String              removeExtensions                = GeneratorProperties
                                                                        .getProperty(GENERATOR_REMOVE_EXTENSIONS);
    private boolean             isCopyBinaryFile                = true;

    private String              includes                        = GeneratorProperties
                                                                        .getProperty(GENERATOR_INCLUDES);         // 需要处理的模板，使用逗号分隔符,示例值: java_src/**,java_test/**
    private String              excludes                        = GeneratorProperties
                                                                        .getProperty(GENERATOR_EXCLUDES);         // 不需要处理的模板，使用逗号分隔符,示例值: java_src/**,java_test/**
    private String              sourceEncoding                  = GeneratorProperties
                                                                        .getProperty(GENERATOR_SOURCE_ENCODING);
    private String              outputEncoding                  = GeneratorProperties
                                                                        .getProperty(GENERATOR_OUTPUT_ENCODING);

    public Generator() {
    }

    public void setTemplateRootDir(File templateRootDir) {
        setTemplateRootDirs(new File[] { templateRootDir });
    }

    /**
     * 设置模板目录，支持用逗号分隔多个模板目录，如 template/rapid,template/company
     * 
     * @param templateRootDir
     */
    public void setTemplateRootDir(String templateRootDir) {
        setTemplateRootDirs(StringHelper.tokenizeToStringArray(templateRootDir, ","));
    }

    public void setTemplateRootDirs(File... templateRootDirs) {
        this.templateRootDirs = new ArrayList<File>(Arrays.asList(templateRootDirs));
    }

    public void setTemplateRootDirs(String... templateRootDirs) {
        ArrayList<File> tempDirs = new ArrayList<File>();
        for (String dir : templateRootDirs) {
            tempDirs.add(FileHelper.getFile(dir));
        }
        this.templateRootDirs = tempDirs;
    }

    public void addTemplateRootDir(File file) {
        templateRootDirs.add(file);
    }

    public void addTemplateRootDir(String file) {
        templateRootDirs.add(FileHelper.getFile(file));
    }

    public boolean isIgnoreTemplateGenerateException() {
        return ignoreTemplateGenerateException;
    }

    public void setIgnoreTemplateGenerateException(boolean ignoreTemplateGenerateException) {
        this.ignoreTemplateGenerateException = ignoreTemplateGenerateException;
    }

    public boolean isCopyBinaryFile() {
        return isCopyBinaryFile;
    }

    public void setCopyBinaryFile(boolean isCopyBinaryFile) {
        this.isCopyBinaryFile = isCopyBinaryFile;
    }

    public String getSourceEncoding() {
        return sourceEncoding;
    }

    public void setSourceEncoding(String sourceEncoding) {
        if (StringHelper.isBlank(sourceEncoding))
            throw new IllegalArgumentException("sourceEncoding must be not empty");
        this.sourceEncoding = sourceEncoding;
    }

    public String getOutputEncoding() {
        return outputEncoding;
    }

    public void setOutputEncoding(String outputEncoding) {
        if (StringHelper.isBlank(outputEncoding))
            throw new IllegalArgumentException("outputEncoding must be not empty");
        this.outputEncoding = outputEncoding;
    }

    public void setIncludes(String includes) {
        this.includes = includes;
    }

    /** 设置不处理的模板路径,可以使用ant类似的值,使用逗号分隔，示例值： **\*.ignore */
    public void setExcludes(String excludes) {
        this.excludes = excludes;
    }

    public void setOutRootDir(String rootDir) {
        if (rootDir == null)
            throw new IllegalArgumentException("outRootDir must be not null");
        this.outRootDir = rootDir;
    }

    public String getOutRootDir() {
        //		if(outRootDir == null) throw new IllegalStateException("'outRootDir' property must be not null.");
        return outRootDir;
    }

    public void setRemoveExtensions(String removeExtensions) {
        this.removeExtensions = removeExtensions;
    }

    public void deleteOutRootDir() throws IOException {
        if (StringHelper.isBlank(getOutRootDir()))
            throw new IllegalStateException("'outRootDir' property must be not null.");
        GLogger.println("[delete dir]    " + getOutRootDir());
        FileHelper.deleteDirectory(new File(getOutRootDir()));
    }

    /**
     * 生成文件
     * 
     * @param templateModel 生成器模板可以引用的变量
     * @param filePathModel 文件路径可以引用的变量
     * @throws Exception
     */
    public Generator generateBy(Map templateModel, Map filePathModel) throws Exception {
        processTemplateRootDirs(templateModel, filePathModel, false);
        return this;
    }

    /**
     * 删除生成的文件
     * 
     * @param templateModel 生成器模板可以引用的变量
     * @param filePathModel 文件路径可以引用的变量
     * @return
     * @throws Exception
     */
    public Generator deleteBy(Map templateModel, Map filePathModel) throws Exception {
        processTemplateRootDirs(templateModel, filePathModel, true);
        return this;
    }

    private void processTemplateRootDirs(Map templateModel, Map filePathModel, boolean isDelete)
            throws Exception {
        if (StringHelper.isBlank(getOutRootDir()))
            throw new IllegalStateException("'outRootDir' property must be not empty.");
        if (templateRootDirs == null || templateRootDirs.size() == 0)
            throw new IllegalStateException("'templateRootDirs'  must be not empty");

        GLogger.debug("******* Template reference variables *********", templateModel);
        GLogger.debug("\n\n******* FilePath reference variables *********", filePathModel);

        //生成 路径值,如 pkg=com.company.project 将生成 pkg_dir=com/company/project的值
        templateModel.putAll(GeneratorHelper.getDirValuesMap(templateModel));
        filePathModel.putAll(GeneratorHelper.getDirValuesMap(filePathModel));

        GeneratorException ge = new GeneratorException("generator occer error, Generator BeanInfo:"
                + BeanHelper.describe(this));
        List<File> processedTemplateRootDirs = processTemplateRootDirs();

        for (int i = 0; i < processedTemplateRootDirs.size(); i++) {
            File templateRootDir = (File) processedTemplateRootDirs.get(i);
            List<Exception> exceptions = scanTemplatesAndProcess(templateRootDir,
                    processedTemplateRootDirs, templateModel, filePathModel, isDelete);
            ge.addAll(exceptions);
        }
        if (!ge.exceptions.isEmpty())
            throw ge;
    }

    /**
     * 用于子类覆盖,预处理模板目录,如执行文件解压动作
     **/
    protected List<File> processTemplateRootDirs() throws Exception {
        return unzipIfTemplateRootDirIsZipFile();
    }

    /**
     * 解压模板目录,如果模板目录是一个zip,jar文件 . 并且支持指定 zip文件的子目录作为模板目录,通过 !号分隔 指定zip文件:
     * c:\\some.zip 指定zip文件子目录: c:\some.zip!/folder/
     * 
     * @throws MalformedURLException
     **/
    private List<File> unzipIfTemplateRootDirIsZipFile() throws MalformedURLException {
        List<File> unzipIfTemplateRootDirIsZipFile = new ArrayList<File>();
        for (int i = 0; i < this.templateRootDirs.size(); i++) {
            File file = templateRootDirs.get(i);
            String templateRootDir = FileHelper.toFilePathIfIsURL(file);

            String subFolder = "";
            int zipFileSeperatorIndexOf = templateRootDir.indexOf("!");
            if (zipFileSeperatorIndexOf >= 0) {
                subFolder = templateRootDir.substring(zipFileSeperatorIndexOf + 1);
                templateRootDir = templateRootDir.substring(0, zipFileSeperatorIndexOf);
            }

            if (new File(templateRootDir).isFile()) {
                File tempDir = ZipUtils.unzip2TempDir(new File(templateRootDir),
                        "tmp_generator_template_folder_for_zipfile");
                unzipIfTemplateRootDirIsZipFile.add(new File(tempDir, subFolder));
            } else {
                unzipIfTemplateRootDirIsZipFile.add(new File(templateRootDir, subFolder));
            }
        }
        return unzipIfTemplateRootDirIsZipFile;
    }

    /**
     * 搜索templateRootDir目录下的所有文件并生成东西
     * 
     * @param templateRootDir 用于搜索的模板目录
     * @param templateRootDirs freemarker用于装载模板的目录
     */
    private List<Exception> scanTemplatesAndProcess(File templateRootDir,
                                                    List<File> templateRootDirs, Map templateModel,
                                                    Map filePathModel, boolean isDelete)
            throws Exception {
        if (templateRootDir == null)
            throw new IllegalStateException("'templateRootDir' must be not null");
        GLogger.println("-------------------load template from templateRootDir = '"
                + templateRootDir.getAbsolutePath() + "' outRootDir:"
                + new File(outRootDir).getAbsolutePath());

        List srcFiles = FileHelper.searchAllNotIgnoreFile(templateRootDir);

        List<Exception> exceptions = new ArrayList();
        for (int i = 0; i < srcFiles.size(); i++) {
            File srcFile = (File) srcFiles.get(i);
            try {
                if (isDelete) {
                    new TemplateProcessor(templateRootDirs).executeDelete(templateRootDir,
                            templateModel, filePathModel, srcFile);
                } else {
                    long start = System.currentTimeMillis();
                    new TemplateProcessor(templateRootDirs).executeGenerate(templateRootDir,
                            templateModel, filePathModel, srcFile);
                    GLogger.perf("genereate by tempate cost time:"
                            + (System.currentTimeMillis() - start) + "ms");
                }
            } catch (Exception e) {
                if (ignoreTemplateGenerateException) {
                    GLogger.warn("iggnore generate error,template is:" + srcFile + " cause:" + e);
                    exceptions.add(e);
                } else {
                    throw e;
                }
            }
        }
        return exceptions;
    }

    /**
     * 单个模板文件的处理器
     **/
    private class TemplateProcessor {
        private GeneratorControl gg               = new GeneratorControl();
        private List<File>       templateRootDirs = new ArrayList<File>();

        public TemplateProcessor(List<File> templateRootDirs) {
            super();
            this.templateRootDirs = templateRootDirs;
        }

        private void executeGenerate(File templateRootDir, Map templateModel, Map filePathModel,
                                     File srcFile) throws SQLException, IOException,
                TemplateException {
            String templateFile = FileHelper.getRelativePath(templateRootDir, srcFile);
            if (GeneratorHelper.isIgnoreTemplateProcess(srcFile, templateFile, includes, excludes)) {
                return;
            }

            if (isCopyBinaryFile && FileHelper.isBinaryFile(srcFile)) {
                String outputFilepath = proceeForOutputFilepath(filePathModel, templateFile);
                File outputFile = new File(getOutRootDir(), outputFilepath);
                GLogger.println("[copy binary file by extention] from:" + srcFile + " => "
                        + outputFile);
                FileHelper.parentMkdir(outputFile);
                IOHelper.copyAndClose(new FileInputStream(srcFile),
                        new FileOutputStream(outputFile));
                return;
            }

            try {
                String outputFilepath = proceeForOutputFilepath(filePathModel, templateFile);

                initGeneratorControlProperties(srcFile, outputFilepath);
                processTemplateForGeneratorControl(templateModel, templateFile);

                if (gg.isIgnoreOutput()) {
                    GLogger.println("[not generate] by gg.isIgnoreOutput()=true on template:"
                            + templateFile);
                    return;
                }

                if (StringHelper.isNotBlank(gg.getOutputFile())) {
                    generateNewFileOrInsertIntoFile(templateFile, gg.getOutputFile(), templateModel);
                }
            } catch (Exception e) {
                throw new RuntimeException("generate oucur error,templateFile is:" + templateFile
                        + " => " + gg.getOutputFile() + " cause:" + e, e);
            }
        }

        private void executeDelete(File templateRootDir, Map templateModel, Map filePathModel,
                                   File srcFile) throws SQLException, IOException,
                TemplateException {
            String templateFile = FileHelper.getRelativePath(templateRootDir, srcFile);
            if (GeneratorHelper.isIgnoreTemplateProcess(srcFile, templateFile, includes, excludes)) {
                return;
            }
            String outputFilepath = proceeForOutputFilepath(filePathModel, templateFile);
            initGeneratorControlProperties(srcFile, outputFilepath);
            gg.deleteGeneratedFile = true;
            processTemplateForGeneratorControl(templateModel, templateFile);
            GLogger.println("[delete file] file:" + new File(gg.getOutputFile()).getAbsolutePath());
            new File(gg.getOutputFile()).delete();
        }

        private void initGeneratorControlProperties(File srcFile, String outputFile)
                throws SQLException {
            gg.setSourceFile(srcFile.getAbsolutePath());
            gg.setSourceFileName(srcFile.getName());
            gg.setSourceDir(srcFile.getParent());
            gg.setOutRoot(getOutRootDir());
            gg.setOutputEncoding(outputEncoding);
            gg.setSourceEncoding(sourceEncoding);
            gg.setMergeLocation(GENERATOR_INSERT_LOCATION);
            gg.setOutputFile(outputFile);
        }

        private void processTemplateForGeneratorControl(Map templateModel, String templateFile)
                throws IOException, TemplateException {
            templateModel.put("gg", gg);
            Template template = getFreeMarkerTemplate(templateFile);
            template.process(templateModel, IOHelper.NULL_WRITER);
        }

        /** 处理文件路径的变量变成输出路径 */
        private String proceeForOutputFilepath(Map filePathModel, String templateFile)
                throws IOException {
            String outputFilePath = templateFile;

            //TODO 删除兼容性的@testExpression
            int testExpressionIndex = -1;
            if ((testExpressionIndex = templateFile.indexOf('@')) != -1) {
                outputFilePath = templateFile.substring(0, testExpressionIndex);
                String testExpressionKey = templateFile.substring(testExpressionIndex + 1);
                Object expressionValue = filePathModel.get(testExpressionKey);
                if (expressionValue == null) {
                    System.err.println("[not-generate] WARN: test expression is null by key:["
                            + testExpressionKey + "] on template:[" + templateFile + "]");
                    return null;
                }
                if (!"true".equals(String.valueOf(expressionValue))) {
                    GLogger.println("[not-generate]\t test expression '@" + testExpressionKey
                            + "' is false,template:" + templateFile);
                    return null;
                }
            }

            for (String removeExtension : removeExtensions.split(",")) {
                if (outputFilePath.endsWith(removeExtension)) {
                    outputFilePath = outputFilePath.substring(0, outputFilePath.length()
                            - removeExtension.length());
                    break;
                }
            }
            Configuration conf = GeneratorHelper.newFreeMarkerConfiguration(templateRootDirs,
                    sourceEncoding, "/filepath/processor/");

            //使freemarker支持过滤,如 ${className?lower_case} 现在为 ${className^lower_case}
            outputFilePath = outputFilePath.replace('^', '?');
            return FreemarkerHelper.processTemplateString(outputFilePath, filePathModel, conf);
        }

        private Template getFreeMarkerTemplate(String templateName) throws IOException {
            return GeneratorHelper.newFreeMarkerConfiguration(templateRootDirs, sourceEncoding,
                    templateName).getTemplate(templateName);
        }

        private void generateNewFileOrInsertIntoFile(String templateFile, String outputFilepath,
                                                     Map templateModel) throws Exception {
            Template template = getFreeMarkerTemplate(templateFile);
            template.setOutputEncoding(gg.getOutputEncoding());

            File absoluteOutputFilePath = FileHelper.parentMkdir(outputFilepath);
            if (absoluteOutputFilePath.exists()) {
                StringWriter newFileContentCollector = new StringWriter();
                if (GeneratorHelper.isFoundInsertLocation(gg, template, templateModel,
                        absoluteOutputFilePath, newFileContentCollector)) {
                    GLogger.println("[insert]\t generate content into:" + outputFilepath);
                    IOHelper.saveFile(absoluteOutputFilePath, newFileContentCollector.toString(),
                            gg.getOutputEncoding());
                    return;
                }
            }

            if (absoluteOutputFilePath.exists() && !gg.isOverride()) {
                GLogger.println("[not generate]\t by gg.isOverride()=false and outputFile exist:"
                        + outputFilepath);
                return;
            }

            if (absoluteOutputFilePath.exists()) {
                GLogger.println("[override]\t template:" + templateFile + " ==> " + outputFilepath);
            } else {
                GLogger.println("[generate]\t template:" + templateFile + " ==> " + outputFilepath);
            }
            FreemarkerHelper.processTemplate(template, templateModel, absoluteOutputFilePath,
                    gg.getOutputEncoding());
        }
    }

    static class GeneratorHelper {

        /**
         * 生成 路径值,如 pkg=com.company.project 将生成 pkg_dir=com/company/project的值
         * 
         * @param map
         * @return
         */
        public static Map getDirValuesMap(Map map) {
            Map dirValues = new HashMap();
            Set<Object> keys = map.keySet();
            for (Object key : keys) {
                Object value = map.get(key);
                if (key instanceof String && value instanceof String) {
                    String dirKey = key + "_dir";
                    String dirValue = value.toString().replace('.', '/');
                    dirValues.put(dirKey, dirValue);
                }
            }
            return dirValues;
        }

        public static boolean isIgnoreTemplateProcess(File srcFile, String templateFile,
                                                      String includes, String excludes) {
            if (srcFile.isDirectory() || srcFile.isHidden())
                return true;
            if (templateFile.trim().equals(""))
                return true;
            if (srcFile.getName().toLowerCase().endsWith(".include")) {
                GLogger.println("[skip]\t\t endsWith '.include' template:" + templateFile);
                return true;
            }
            templateFile = templateFile.replace('\\', '/');
            for (String exclude : StringHelper.tokenizeToStringArray(excludes, ",")) {
                if (new AntPathMatcher().match(exclude.replace('\\', '/'), templateFile))
                    return true;
            }
            if (StringHelper.isBlank(includes)) {
                return false;
            }
            for (String include : StringHelper.tokenizeToStringArray(includes, ",")) {
                if (new AntPathMatcher().match(include.replace('\\', '/'), templateFile))
                    return false;
            }
            return true;
        }

        private static boolean isFoundInsertLocation(GeneratorControl gg, Template template,
                                                     Map model, File outputFile,
                                                     StringWriter newFileContent)
                throws IOException, TemplateException {
            LineNumberReader reader = new LineNumberReader(new FileReader(outputFile));
            String line = null;
            boolean isFoundInsertLocation = false;

            //FIXME 持续性的重复生成会导致out of memory
            PrintWriter writer = new PrintWriter(newFileContent);
            while ((line = reader.readLine()) != null) {
                writer.println(line);
                // only insert once
                if (!isFoundInsertLocation && line.indexOf(gg.getMergeLocation()) >= 0) {
                    template.process(model, writer);
                    writer.println();
                    isFoundInsertLocation = true;
                }
            }

            writer.close();
            reader.close();
            return isFoundInsertLocation;
        }

        public static Configuration newFreeMarkerConfiguration(List<File> templateRootDirs,
                                                               String defaultEncoding,
                                                               String templateName)
                throws IOException {
            Configuration conf = new Configuration();

            FileTemplateLoader[] templateLoaders = new FileTemplateLoader[templateRootDirs.size()];
            for (int i = 0; i < templateRootDirs.size(); i++) {
                templateLoaders[i] = new FileTemplateLoader((File) templateRootDirs.get(i));
            }
            MultiTemplateLoader multiTemplateLoader = new MultiTemplateLoader(templateLoaders);

            conf.setTemplateLoader(multiTemplateLoader);
            conf.setNumberFormat("###############");
            conf.setBooleanFormat("true,false");
            conf.setDefaultEncoding(defaultEncoding);

            //			String autoIncludes = new File(new File(templateName).getParent(),"macro.include").getPath();
            //			List<String> availableAutoInclude = FreemarkerHelper.getAvailableAutoInclude(conf, Arrays.asList("macro.include",autoIncludes));
            //			conf.setAutoIncludes(availableAutoInclude);
            //			GLogger.info("[set Freemarker.autoIncludes]"+availableAutoInclude+" for templateName:"+templateName);

            List<String> autoIncludes = getParentPaths(templateName, "macro.include");
            List<String> availableAutoInclude = FreemarkerHelper.getAvailableAutoInclude(conf,
                    autoIncludes);
            conf.setAutoIncludes(availableAutoInclude);
            GLogger.trace("set Freemarker.autoIncludes:" + availableAutoInclude
                    + " for templateName:" + templateName + " autoIncludes:" + autoIncludes);
            return conf;
        }

        public static List<String> getParentPaths(String templateName, String suffix) {
            String array[] = StringHelper.tokenizeToStringArray(templateName, "\\/");
            List<String> list = new ArrayList<String>();
            list.add(suffix);
            list.add(File.separator + suffix);
            String path = "";
            for (int i = 0; i < array.length; i++) {
                path = path + File.separator + array[i];
                list.add(path + File.separator + suffix);
            }
            return list;
        }
    }

    @SuppressWarnings("unchecked")
    public static class GeneratorModel implements java.io.Serializable {
        private static final long serialVersionUID = -6430787906037836995L;
        /** 用于存放'模板'可以引用的变量 */
        public Map                templateModel    = new HashMap();
        /** 用于存放'文件路径'可以引用的变量 */
        public Map                filePathModel    = new HashMap();

        public GeneratorModel() {
        }

        public GeneratorModel(Map templateModel, Map filePathModel) {
            this.templateModel = templateModel;
            this.filePathModel = filePathModel;
        }

    }
}
